From: Colin Walters Date: Wed, 18 Jun 2025 19:10:53 +0000 (-0400) Subject: Add ostree admin prepare-soft-reboot X-Git-Tag: archive/raspbian/2025.7-2+rpi1^2^2~6^2~4^2~14^2 X-Git-Url: https://dgit.raspbian.org/%22http:/www.example.com/cgi/%22https://%22%22/%22http:/www.example.com/cgi/%22https:/%22%22?a=commitdiff_plain;h=fafca2f3aae43e7ccde65a7100bdf88e4a1a8f96;p=ostree.git Add ostree admin prepare-soft-reboot This adds support for systemd soft reboots. Closes: https://github.com/ostreedev/ostree/issues/3242 Signed-off-by: Colin Walters Co-authored-by: Joseph Marrero Corchado Co-authored-by: Mary Strodl Signed-off-by: Colin Walters --- diff --git a/Makefile-libostree.am b/Makefile-libostree.am index b84ac0e5..463b809a 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -107,6 +107,7 @@ libostree_1_la_SOURCES = \ src/libostree/ostree-sysroot-cleanup.c \ src/libostree/ostree-sysroot-deploy.c \ src/libostree/ostree-sysroot-upgrader.c \ + src/libostree/ostree-soft-reboot.c \ src/libostree/ostree-impl-system-generator.c \ src/libostree/ostree-bootconfig-parser.c \ src/libostree/ostree-deployment.c \ @@ -175,9 +176,9 @@ endif # USE_GPGME symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym # Uncomment this include when adding new development symbols. -#if BUILDOPT_IS_DEVEL_BUILD -#symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym -#endif +if BUILDOPT_IS_DEVEL_BUILD +symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym +endif # http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html wl_versionscript_arg = -Wl,--version-script= diff --git a/Makefile-man.am b/Makefile-man.am index 5d1b9d48..b36f3af1 100644 --- a/Makefile-man.am +++ b/Makefile-man.am @@ -30,6 +30,7 @@ ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-stateroot-init.1 ost ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1 \ ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin-unlock.1 \ ostree-admin-pin.1 ostree-admin-post-copy.1 ostree-admin-set-default.1 \ +ostree-admin-prepare-soft-reboot.1 \ ostree-admin-lock-finalization.1 \ ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1 \ ostree-commit.1 ostree-create-usb.1 ostree-export.1 \ diff --git a/Makefile-ostree.am b/Makefile-ostree.am index d2447ffe..6076ae51 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -83,6 +83,8 @@ ostree_SOURCES += \ src/ostree/ot-admin-builtin-switch.c \ src/ostree/ot-admin-builtin-pin.c \ src/ostree/ot-admin-builtin-post-copy.c \ + src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c \ + src/ostree/ot-admin-builtin-prepare-soft-reboot.c \ src/ostree/ot-admin-builtin-upgrade.c \ src/ostree/ot-admin-builtin-unlock.c \ src/ostree/ot-admin-builtin-state-overlay.c \ diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index 3a46d7e0..2ad402cb 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -192,6 +192,7 @@ ostree_deployment_get_unlocked ostree_deployment_is_pinned ostree_deployment_is_staged ostree_deployment_is_finalization_locked +ostree_deployment_is_soft_reboot_target ostree_deployment_set_index ostree_deployment_set_bootserial ostree_deployment_set_bootconfig @@ -589,6 +590,8 @@ ostree_sysroot_deployment_set_kargs_in_place ostree_sysroot_deployment_set_mutable ostree_sysroot_deployment_unlock ostree_sysroot_deployment_set_pinned +ostree_sysroot_deployment_can_soft_reboot +ostree_sysroot_deployment_prepare_next_root ostree_sysroot_write_deployments ostree_sysroot_write_deployments_with_options ostree_sysroot_write_origin_file diff --git a/man/ostree-admin-prepare-soft-reboot.xml b/man/ostree-admin-prepare-soft-reboot.xml new file mode 100644 index 00000000..fa34457b --- /dev/null +++ b/man/ostree-admin-prepare-soft-reboot.xml @@ -0,0 +1,51 @@ + + + + + + + + + ostree admin prepare-soft-reboot + OSTree + + + + ostree admin prepare-soft-reboot + 1 + + + + ostree-admin-prepare-soft-reboot + Prepare the target deployment (via index) for soft reboot + + + + + ostree admin prepare-soft-reboot INDEX + + + + + Description + + + Prepare the deployment at INDEX for a systemd soft reboot. INDEX must be in range and not reference the currently booted deployment. + It is recommended to immediately follow this with an involcation of systemctl soft-reboot. + + + + It is not supported to soft reboot into a deployment with a different kernel than the booted one. + + + + + See Also + + systemd-soft-reboot.service8 + + + diff --git a/src/boot/ostree-boot-complete.service b/src/boot/ostree-boot-complete.service index 98ae3d18..e8b4f42f 100644 --- a/src/boot/ostree-boot-complete.service +++ b/src/boot/ostree-boot-complete.service @@ -21,6 +21,8 @@ ConditionKernelCommandLine=ostree # marked as a triggering condition in case in the future we want # to do something else. ConditionPathExists=|/boot/ostree/finalize-failure.stamp +# Also run when soft-reboot cleanup is needed +ConditionPathExists=|/run/ostree/nextroot-booted # We start early DefaultDependencies=no After=sysinit.target diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 6640e11c..4a46a0b3 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -29,3 +29,10 @@ global: someostree_symbol_deleteme; } LIBOSTREE_$YEAR.$LASTSTABLE; */ + +LIBOSTREE_2025.3 { +global: + ostree_deployment_is_soft_reboot_target; + ostree_sysroot_deployment_can_soft_reboot; + ostree_sysroot_deployment_prepare_next_root; +} LIBOSTREE_2025.2; diff --git a/src/libostree/ostree-cmd-private.c b/src/libostree/ostree-cmd-private.c index 239d3cd3..f40d3daa 100644 --- a/src/libostree/ostree-cmd-private.c +++ b/src/libostree/ostree-cmd-private.c @@ -50,6 +50,7 @@ ostree_cmd__private__ (void) _ostree_repo_static_delta_dump, _ostree_repo_static_delta_query_exists, _ostree_repo_static_delta_delete, _ostree_repo_verify_bindings, _ostree_sysroot_finalize_staged, _ostree_sysroot_boot_complete, + _ostree_prepare_soft_reboot, }; return &table; diff --git a/src/libostree/ostree-cmd-private.h b/src/libostree/ostree-cmd-private.h index 3b48e6ee..9bd7c4af 100644 --- a/src/libostree/ostree-cmd-private.h +++ b/src/libostree/ostree-cmd-private.h @@ -45,6 +45,7 @@ typedef struct GError **error); gboolean (*ostree_boot_complete) (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error); + gboolean (*ostree_prepare_soft_reboot) (GError **error); } OstreeCmdPrivateVTable; /* Note this not really "public", we just export the symbol, but not the header */ diff --git a/src/libostree/ostree-deployment-private.h b/src/libostree/ostree-deployment-private.h index f6766c39..55a3bded 100644 --- a/src/libostree/ostree-deployment-private.h +++ b/src/libostree/ostree-deployment-private.h @@ -52,6 +52,7 @@ struct _OstreeDeployment OstreeDeploymentUnlockedState unlocked; gboolean staged; gboolean finalization_locked; + gboolean soft_reboot_target; char **overlay_initrds; char *overlay_initrds_id; }; diff --git a/src/libostree/ostree-deployment.c b/src/libostree/ostree-deployment.c index 8be2fdd5..b4427262 100644 --- a/src/libostree/ostree-deployment.c +++ b/src/libostree/ostree-deployment.c @@ -476,3 +476,16 @@ ostree_deployment_is_finalization_locked (OstreeDeployment *self) { return self->finalization_locked; } + +/** + * ostree_deployment_is_soft_reboot_target: + * @self: Deployment + * + * Returns: `TRUE` if deployment is set for a soft reboot. + * Since: TODO + */ +gboolean +ostree_deployment_is_soft_reboot_target (OstreeDeployment *self) +{ + return self->soft_reboot_target; +} diff --git a/src/libostree/ostree-deployment.h b/src/libostree/ostree-deployment.h index 0536d981..7e923fe8 100644 --- a/src/libostree/ostree-deployment.h +++ b/src/libostree/ostree-deployment.h @@ -73,6 +73,8 @@ gboolean ostree_deployment_is_staged (OstreeDeployment *self); _OSTREE_PUBLIC gboolean ostree_deployment_is_finalization_locked (OstreeDeployment *self); _OSTREE_PUBLIC +gboolean ostree_deployment_is_soft_reboot_target (OstreeDeployment *self); +_OSTREE_PUBLIC gboolean ostree_deployment_is_pinned (OstreeDeployment *self); _OSTREE_PUBLIC diff --git a/src/libostree/ostree-soft-reboot.c b/src/libostree/ostree-soft-reboot.c new file mode 100644 index 00000000..08fc909b --- /dev/null +++ b/src/libostree/ostree-soft-reboot.c @@ -0,0 +1,99 @@ +/* -*- c-file-style: "gnu" -*- + * Soft reboot for ostree. This code was originally derived from ostree-prepare-root.c, + * but is now significantly cut down to target specifically soft rebooting. + * + * SPDX-License-Identifier: LGPL-2.0+ + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "ostree-mount-util.h" +#include "ot-keyfile-utils.h" +#include "otcore.h" + +/* This key configures the / mount in the deployment root */ +#define ROOT_KEY "root" +#define ETC_KEY "etc" +#define TRANSIENT_KEY "transient" + +gboolean +_ostree_prepare_soft_reboot (GError **error) +{ + const char *sysroot_path = "/sysroot"; + const char *target_deployment = "."; + + g_autoptr (GKeyFile) config = otcore_load_config (AT_FDCWD, PREPARE_ROOT_CONFIG_PATH, error); + if (!config) + return FALSE; + + gboolean root_transient = FALSE; + if (!ot_keyfile_get_boolean_with_default (config, ROOT_KEY, TRANSIENT_KEY, FALSE, &root_transient, + error)) + return FALSE; + + g_autofree char *kernel_cmdline = read_proc_cmdline (); + g_autoptr (ComposefsConfig) composefs_config + = otcore_load_composefs_config (kernel_cmdline, config, TRUE, error); + if (!composefs_config) + return FALSE; + + if (composefs_config->enabled != OT_TRISTATE_YES) + return glnx_throw (error, "soft reboot not supported without composefs"); + + GVariantBuilder metadata_builder; + g_variant_builder_init (&metadata_builder, G_VARIANT_TYPE ("a{sv}")); + + if (!glnx_shutil_mkdir_p_at (AT_FDCWD, OTCORE_RUN_NEXTROOT, 0755, NULL, error)) + return FALSE; + + // Tracks if we did successfully enable it at runtime + bool using_composefs = false; + if (!otcore_mount_rootfs (composefs_config, &metadata_builder, root_transient, sysroot_path, + target_deployment, OTCORE_RUN_NEXTROOT, &using_composefs, error)) + return glnx_prefix_error (error, "failed to mount composefs"); + + if (!using_composefs) + return glnx_throw (error, "failed to mount with composefs"); + + if (!otcore_mount_etc (config, &metadata_builder, OTCORE_RUN_NEXTROOT, error)) + return FALSE; + + // Note we should have inherited the readonly sysroot + g_autofree char *target_sysroot = g_build_filename (OTCORE_RUN_NEXTROOT, "sysroot", NULL); + if (mount (sysroot_path, target_sysroot, NULL, MS_BIND | MS_SILENT, NULL) < 0) + return glnx_throw_errno_prefix (error, "failed to bind mount sysroot"); + + /* This can be used by other things to signal ostree is in use */ + { + g_autoptr (GVariant) metadata = g_variant_ref_sink (g_variant_builder_end (&metadata_builder)); + const guint8 *buf = g_variant_get_data (metadata) ?: (guint8 *)""; + if (!glnx_file_replace_contents_at (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, buf, + g_variant_get_size (metadata), 0, NULL, error)) + return FALSE; + } + + return TRUE; +} diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index a57a812d..2d676be1 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -4108,10 +4108,20 @@ _ostree_sysroot_boot_complete (OstreeSysroot *self, GCancellable *cancellable, G if (!ot_openat_ignore_enoent (self->boot_fd, _OSTREE_FINALIZE_STAGED_FAILURE_PATH, &failure_fd, error)) return FALSE; - // If we didn't find a failure log, then there's nothing to do right now. - // (Actually this unit shouldn't even be invoked, but we may do more in the future) + // If we didn't find a failure log, check for soft-reboot completion tasks if (failure_fd == -1) - return TRUE; + { + // Check if we just completed a soft-reboot and need to update /run/ostree-booted + // We're completing a soft-reboot, simply move the nextroot-booted file to ostree-booted + if (rename (OTCORE_RUN_NEXTROOT_BOOTED, OTCORE_RUN_BOOTED) < 0) + { + if (errno != ENOENT) + return glnx_throw_errno_prefix (error, "Failed to rename %s to %s", + OTCORE_RUN_NEXTROOT_BOOTED, OTCORE_RUN_BOOTED); + g_debug ("Updated /run/ostree-booted for soft-reboot completion"); + } + return TRUE; + } g_autofree char *failure_data = glnx_fd_readall_utf8 (failure_fd, NULL, cancellable, error); if (failure_data == NULL) return glnx_prefix_error (error, "Reading from %s", _OSTREE_FINALIZE_STAGED_FAILURE_PATH); @@ -4270,6 +4280,119 @@ ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, OstreeDeployment *de return TRUE; } +struct PrepareRootChildSetupContext +{ + const char *deployment_path; + int rootns_fd; +}; + +static inline void +prepare_root_child_setup (gpointer data) +{ + struct PrepareRootChildSetupContext *ctx = data; + // Enter the root namespace first to escape the overlayfs context + int rc = setns (ctx->rootns_fd, CLONE_NEWNS); + if (rc < 0) + err (1, "setns"); + // Then change to the deployment directory in the root namespace + rc = chdir (ctx->deployment_path); + if (rc < 0) + err (1, "chdir"); +} + +/** + * ostree_sysroot_deployment_can_soft_reboot: + * @self: The #OstreeSysroot object. + * @deployment: The #OstreeDeployment to check for soft-reboot compatibility. + * + * Checks if the given deployment can be soft-rebooted to from the currently + * booted deployment. A soft-reboot is generally only possible if both the + * currently booted deployment and the target `deployment` use the same kernel + * (i.e., have the same boot checksum). + * + * Returns: %TRUE if a soft-reboot is possible to the target deployment, %FALSE otherwise. + * Since: TODO + */ +gboolean +ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment *deployment) +{ + OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (self); + if (booted_deployment != NULL) + { + const char *booted_bootcsum = ostree_deployment_get_bootcsum (booted_deployment); + const char *target_bootcsum = ostree_deployment_get_bootcsum (deployment); + return g_str_equal (booted_bootcsum, target_bootcsum); + } + return false; +} + +/** + * ostree_sysroot_deployment_prepare_next_root + * @self: Sysroot + * @deployment: Deployment to prepare /run/nextroot + * @allow_kernel_skew: Continue even if there is a kernel mismatch + * @cancellable: Cancellable + * @error: Error + * + * Prepare the specified deployment for a systemd soft-reboot by creating a new + * root with it at `/run/nextroot`. + * + * Since: TODO + */ +gboolean +ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, OstreeDeployment *deployment, + gboolean allow_kernel_skew, GCancellable *cancellable, + GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("Preparing /run/nextroot for a soft-reboot", error); + + if (!ostree_sysroot_deployment_can_soft_reboot (self, deployment) && !allow_kernel_skew) + { + return glnx_throw (error, "Cannot soft-reboot to deployment with different kernel"); + } + + // For targeting a staged deployment, we finalize now to ensure that we have /etc + if (ostree_deployment_is_staged (deployment)) + { + if (!_ostree_sysroot_finalize_staged (self, NULL, error)) + return FALSE; + } + + g_autofree char *deployment_relpath = ostree_sysroot_get_deployment_dirpath (self, deployment); + g_autofree char *deployment_fullpath = g_build_filename ("/sysroot", deployment_relpath, NULL); + gint estatus; + + const char *argv[] = { "ostree", "admin", "impl-prepare-soft-reboot", NULL }; + + glnx_autofd int rootns_fd = -1; + if (!glnx_openat_rdonly (AT_FDCWD, "/proc/1/ns/mnt", TRUE, &rootns_fd, error)) + return FALSE; + + struct PrepareRootChildSetupContext ctx = { + .deployment_path = deployment_fullpath, + .rootns_fd = rootns_fd, + }; + + if (!g_spawn_sync (NULL, (char **)argv, NULL, G_SPAWN_SEARCH_PATH, prepare_root_child_setup, &ctx, + NULL, NULL, &estatus, error)) + return FALSE; + + if (!g_spawn_check_exit_status (estatus, error)) + { + int flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL; + // If we failed to initialize the soft reboot, ensure that we've unwound any mounts + const char *umount_argv[] = { "umount", "-R", "/run/nextroot", NULL }; + // To aid debugging allow skipping cleanup on failure + if (!g_getenv ("OSTREE_SKIP_NEXTROOT_CLEANUP")) + g_spawn_sync (NULL, (char **)umount_argv, NULL, flags, NULL, NULL, NULL, NULL, NULL, NULL); + return FALSE; + } + + ot_journal_print (LOG_INFO, "Set up soft reboot at /run/nextroot"); + + return TRUE; +} + /** * ostree_sysroot_deployment_kexec_load * @self: Sysroot diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h index 9a6566d5..6370592f 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -82,6 +82,10 @@ struct OstreeSysroot /* The device/inode for / and /etc, used to detect booted deployment */ dev_t root_device; ino_t root_inode; + /* The device inode for a queued soft reboot deployment */ + gboolean have_nextroot; + dev_t nextroot_device; + ino_t nextroot_inode; // The parsed data from /run/ostree GVariantDict *run_ostree_metadata; @@ -151,6 +155,8 @@ gboolean _ostree_sysroot_finalize_staged (OstreeSysroot *self, GCancellable *can gboolean _ostree_sysroot_boot_complete (OstreeSysroot *self, GCancellable *cancellable, GError **error); +gboolean _ostree_prepare_soft_reboot (GError **error); + OstreeDeployment *_ostree_sysroot_deserialize_deployment_from_variant (GVariant *v, GError **error); char *_ostree_sysroot_get_deployment_backing_relpath (OstreeDeployment *deployment); diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index e45a71f6..f9955ce2 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -867,16 +867,16 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment * if (!glnx_opendirat (self->sysroot_fd, relative_boot_link, TRUE, &deployment_dfd, error)) return FALSE; + struct stat stbuf; + if (!glnx_fstat (deployment_dfd, &stbuf, error)) + return FALSE; + /* See if this is the booted deployment */ const gboolean looking_for_booted_deployment = (self->root_is_ostree_booted && !self->booted_deployment); gboolean is_booted_deployment = FALSE; if (looking_for_booted_deployment) { - struct stat stbuf; - if (!glnx_fstat (deployment_dfd, &stbuf, error)) - return FALSE; - /* ostree-prepare-root records the (device, inode) pair of the underlying real deployment * directory (before we might have mounted a composefs or overlayfs on top). * @@ -904,6 +904,9 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment * is_booted_deployment = stbuf.st_dev == expected_root_dev && stbuf.st_ino == expected_root_inode; } + gboolean is_soft_reboot_target + = self->have_nextroot + && (stbuf.st_dev == self->nextroot_device && stbuf.st_ino == self->nextroot_inode); g_autoptr (OstreeDeployment) ret_deployment = ostree_deployment_new (-1, osname, treecsum, deployserial, bootcsum, treebootserial); @@ -915,7 +918,6 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment * ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT); g_autofree char *unlocked_transient_path = _ostree_sysroot_get_runstate_path ( ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_TRANSIENT); - struct stat stbuf; if (lstat (unlocked_development_path, &stbuf) == 0) ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT; else if (lstat (unlocked_transient_path, &stbuf) == 0) @@ -932,6 +934,7 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment * } /* TODO: warn on unknown unlock types? */ } + ret_deployment->soft_reboot_target = is_soft_reboot_target; g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked); @@ -1137,7 +1140,18 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error) if (!self->root_is_ostree_booted) return TRUE; /* Note early return */ - g_assert (self->booted_deployment); + /* In normal cases, we should have a booted deployment. However, during + * soft-reboot scenarios, the current deployment may not correspond to + * any bootloader entry, so booted_deployment could be NULL. */ + if (!self->booted_deployment) + { + /* Check if we're in a soft-reboot scenario */ + if (!(g_file_test ("/run/nextroot", G_FILE_TEST_IS_DIR) + && g_file_test ("/run/nextroot/sysroot", G_FILE_TEST_IS_DIR))) + { + g_assert (self->booted_deployment); + } + } g_clear_object (&self->staged_deployment); g_clear_pointer (&self->staged_deployment_data, g_variant_unref); @@ -1190,6 +1204,44 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error) return TRUE; } +/* Reload state from /run/ostree/nextroot-booted */ +static gboolean +_ostree_sysroot_reload_soft_reboot (OstreeSysroot *self, GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("Loading nextroot", error); + // Reset state + self->have_nextroot = FALSE; + + glnx_autofd int fd = -1; + if (!ot_openat_ignore_enoent (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, &fd, error)) + return FALSE; + // If there's no such file, we're done + if (fd == -1) + return TRUE; + + // Parse the GVariant metadata from this; search for OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO + // to find similar code. + g_autoptr (GVariant) metadata = NULL; + if (!ot_variant_read_fd (fd, 0, G_VARIANT_TYPE_VARDICT, TRUE, &metadata, error)) + return glnx_prefix_error (error, "failed to read %s", OTCORE_RUN_NEXTROOT_BOOTED); + + // Get the backing device/inode from metadata + guint64 backing_dev, backing_ino; + g_autoptr (GVariant) backing_devino = g_variant_lookup_value ( + metadata, OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO, G_VARIANT_TYPE ("(tt)")); + if (!backing_devino) + return glnx_throw (error, "Missing %s key in %s", OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO, + OTCORE_RUN_NEXTROOT_BOOTED); + + // Load the device/inode, and we're done + g_variant_get (backing_devino, "(tt)", &backing_dev, &backing_ino); + self->have_nextroot = TRUE; + self->nextroot_device = (dev_t)backing_dev; + self->nextroot_inode = (ino_t)backing_ino; + + return TRUE; +} + /* Loads the current bootversion, subbootversion, and deployments, starting from the * bootloader configs which are the source of truth. */ @@ -1208,6 +1260,9 @@ sysroot_load_from_bootloader_configs (OstreeSysroot *self, GCancellable *cancell error)) return FALSE; + if (!_ostree_sysroot_reload_soft_reboot (self, error)) + return FALSE; + g_autoptr (GPtrArray) boot_loader_configs = NULL; if (!_ostree_sysroot_read_boot_loader_configs (self, bootversion, &boot_loader_configs, cancellable, error)) diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h index 64a1207c..327c6d45 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -268,6 +268,15 @@ gboolean ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, const c OstreeSysrootSimpleWriteDeploymentFlags flags, GCancellable *cancellable, GError **error); +_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, + OstreeDeployment *deployment); + +_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, + OstreeDeployment *deployment, + gboolean allow_kernel_skew, + GCancellable *cancellable, + GError **error); + _OSTREE_PUBLIC gboolean ostree_sysroot_deployment_kexec_load (OstreeSysroot *self, OstreeDeployment *deployment, GCancellable *cancellable, GError **error); diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h index 01468b1f..24b4d6cb 100644 --- a/src/libotcore/otcore.h +++ b/src/libotcore/otcore.h @@ -133,9 +133,14 @@ gboolean otcore_mount_etc (GKeyFile *config, GVariantBuilder *metadata_builder, #define OTCORE_PREPARE_ROOT_KEYPATH_KEY "keypath" #define OTCORE_PREPARE_ROOT_TRANSIENT_KEY "transient" +// For use with systemd soft reboots +#define OTCORE_RUN_NEXTROOT "/run/nextroot" + // The file written in the initramfs which contains an a{sv} of metadata // from ostree-prepare-root. #define OTCORE_RUN_BOOTED "/run/ostree-booted" +// Written by ostree-soft-reboot.c with metadata about /run/nextroot +#define OTCORE_RUN_NEXTROOT_BOOTED "/run/ostree/nextroot-booted" // This key will be present if composefs was successfully used. #define OTCORE_RUN_BOOTED_KEY_COMPOSEFS "composefs" // True if fsverity was required for composefs. diff --git a/src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c b/src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c new file mode 100644 index 00000000..c4d9553f --- /dev/null +++ b/src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: LGPL-2.0+ + */ + +#include "config.h" + +#include + +#include "ostree.h" +#include "ot-admin-builtins.h" +#include "ot-admin-functions.h" +#include "otutil.h" + +#include "ostree-cmd-private.h" + +gboolean +ot_admin_builtin_impl_prepare_soft_reboot (int argc, char **argv, + OstreeCommandInvocation *invocation, + GCancellable *cancellable, GError **error) +{ + if (!ostree_cmd__private__ ()->ostree_prepare_soft_reboot (error)) + return FALSE; + + return TRUE; +} diff --git a/src/ostree/ot-admin-builtin-prepare-soft-reboot.c b/src/ostree/ot-admin-builtin-prepare-soft-reboot.c new file mode 100644 index 00000000..3151f6e2 --- /dev/null +++ b/src/ostree/ot-admin-builtin-prepare-soft-reboot.c @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 Colin Walters + * + * SPDX-License-Identifier: LGPL-2.0+ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include "config.h" + +#include + +#include "ostree.h" +#include "ot-admin-builtins.h" +#include "ot-admin-functions.h" +#include "otutil.h" + +static GOptionEntry options[] = { { NULL } }; + +gboolean +ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvocation *invocation, + GCancellable *cancellable, GError **error) +{ + g_autoptr (GOptionContext) context = g_option_context_new ("INDEX"); + + g_autoptr (OstreeSysroot) sysroot = NULL; + if (!ostree_admin_option_context_parse (context, options, &argc, &argv, + OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER, invocation, &sysroot, + cancellable, error)) + return FALSE; + + if (argc < 2) + { + ot_util_usage_error (context, "INDEX must be specified", error); + return FALSE; + } + + const char *deploy_index_str = argv[1]; + guint deploy_index; + { + char *endptr = NULL; + errno = 0; + deploy_index = (guint)g_ascii_strtoull (deploy_index_str, &endptr, 10); + if (*endptr != '\0') + return glnx_throw (error, "Invalid index: %s", deploy_index_str); + } + + g_autoptr (OstreeDeployment) target_deployment + = ot_admin_get_indexed_deployment (sysroot, deploy_index, error); + if (!target_deployment) + return FALSE; + + if (target_deployment == ostree_sysroot_get_booted_deployment (sysroot)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Cannot prepare for soft-reboot currently booted deployment %i", deploy_index); + return FALSE; + } + + if (!ostree_sysroot_deployment_prepare_next_root (sysroot, target_deployment, FALSE, cancellable, + error)) + return FALSE; + + return TRUE; +} diff --git a/src/ostree/ot-admin-builtin-status.c b/src/ostree/ot-admin-builtin-status.c index d05d9928..485c8d9c 100644 --- a/src/ostree/ot-admin-builtin-status.c +++ b/src/ostree/ot-admin-builtin-status.c @@ -41,6 +41,7 @@ static GOptionEntry options[] "Output \"default\" if booted into the default deployment, otherwise \"not-default\"", NULL }, { NULL } }; + static gboolean deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployment *deployment, gboolean is_booted, gboolean is_pending, gboolean is_rollback, @@ -77,18 +78,24 @@ deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploym g_autofree char *origin_refspec = origin ? g_key_file_get_string (origin, "origin", "refspec", NULL) : NULL; - const char *deployment_status = ""; + g_autoptr (GString) deployment_status = g_string_new (""); + if (ostree_deployment_is_finalization_locked (deployment)) - deployment_status = " (finalization locked)"; + g_string_append (deployment_status, " (finalization locked)"); else if (ostree_deployment_is_staged (deployment)) - deployment_status = " (staged)"; + g_string_append (deployment_status, " (staged)"); else if (is_pending) - deployment_status = " (pending)"; + g_string_append (deployment_status, " (pending)"); else if (is_rollback) - deployment_status = " (rollback)"; - g_print ("%c %s %s.%d%s\n", is_booted ? '*' : ' ', ostree_deployment_get_osname (deployment), + g_string_append (deployment_status, " (rollback)"); + + if (ostree_deployment_is_soft_reboot_target (deployment)) + g_string_append (deployment_status, " (soft-reboot)"); + + char deployment_marker = is_booted ? '*' : ' '; + g_print ("%c %s %s.%d%s\n", deployment_marker, ostree_deployment_get_osname (deployment), ostree_deployment_get_csum (deployment), ostree_deployment_get_deployserial (deployment), - deployment_status); + deployment_status->str); if (version) g_print (" Version: %s\n", version); diff --git a/src/ostree/ot-admin-builtins.h b/src/ostree/ot-admin-builtins.h index 5e94b8a6..f873e6bd 100644 --- a/src/ostree/ot-admin-builtins.h +++ b/src/ostree/ot-admin-builtins.h @@ -41,6 +41,8 @@ BUILTINPROTO (cleanup); BUILTINPROTO (pin); BUILTINPROTO (finalize_staged); BUILTINPROTO (boot_complete); +BUILTINPROTO (prepare_soft_reboot); +BUILTINPROTO (impl_prepare_soft_reboot); BUILTINPROTO (unlock); BUILTINPROTO (status); BUILTINPROTO (set_origin); diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c index 7488fe07..a89afb2b 100644 --- a/src/ostree/ot-builtin-admin.c +++ b/src/ostree/ot-builtin-admin.c @@ -42,6 +42,8 @@ static OstreeCommand admin_subcommands[] = { "Change the finalization locking state of the staged deployment" }, { "boot-complete", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN, ot_admin_builtin_boot_complete, "Internal command to run at boot after an update was applied" }, + { "impl-prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN, + ot_admin_builtin_impl_prepare_soft_reboot, "Internal command to prepare soft reboot" }, { "state-overlay", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN, ot_admin_builtin_state_overlay, "Internal command to assemble a state overlay" }, { "init-fs", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_init_fs, @@ -57,6 +59,8 @@ static OstreeCommand admin_subcommands[] = { "rollback strings" }, { "post-copy", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_post_copy, "Update the repo and deployments as needed after a copy" }, + { "prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_prepare_soft_reboot, + "Prepare deployment for soft-reboot" }, { "set-origin", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_set_origin, "Set Origin and create a new origin file" }, { "status", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_status, "List deployments" }, diff --git a/tests/kolainst/destructive/soft-reboot.sh b/tests/kolainst/destructive/soft-reboot.sh new file mode 100755 index 00000000..25624c03 --- /dev/null +++ b/tests/kolainst/destructive/soft-reboot.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -xeuo pipefail + +. ${KOLA_EXT_DATA}/libinsttest.sh + +require_writable_sysroot +prepare_tmpdir + +case "${AUTOPKGTEST_REBOOT_MARK:-}" in + "") + # xref https://github.com/coreos/coreos-assembler/pull/2814 + systemctl mask --now zincati + + assert_streq $(systemctl show -P SoftRebootsCount) 0 + + # Create a synthetic commit for upgrade + cd /ostree/repo/tmp + ostree checkout -H ${host_commit} t + unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && cd /ostree/repo/tmp/t && touch usr/etc/new-file-for-soft-reboot usr/share/test-file-for-soft-reboot' + ostree commit --no-bindings --parent="${host_commit}" -b soft-reboot-test -I --consume t + newcommit=$(ostree rev-parse soft-reboot-test) + # Deploy the new commit normally first + ostree admin deploy --stage soft-reboot-test + + # Test prepare-soft-reboot command + echo "Testing prepare-soft-reboot..." + ostree admin prepare-soft-reboot 0 + + ostree admin status > status.txt + assert_file_has_content_literal status.txt '(pending) (soft-reboot)' + + test -f /run/ostree/nextroot-booted + + /tmp/autopkgtest-soft-reboot "2" + ;; + "2") + # After soft reboot, verify we're running the new deployment + echo "Verifying post-soft-reboot state..." + assert_streq $(systemctl show -P SoftRebootsCount) 1 + + expected_commit=$(ostree rev-parse soft-reboot-test) + + if [ "${host_commit}" != "${expected_commit}" ]; then + echo "ERROR: Expected commit ${host_commit}, but got ${current_commit}" + exit 1 + fi + + test -f /etc/new-file-for-soft-reboot + test -f /usr/share/test-file-for-soft-reboot + + # Verify that soft-reboot-pending file is cleaned up + test '!' -f /run/ostree/nextroot-booted + + echo "Soft reboot test completed successfully!" + ;; + *) + fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}" + ;; +esac